﻿using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Reflection;

namespace SimpleMapper
{
    
    /// <summary>
    /// Maps objects from one type to another by mapping matched properties
    /// </summary>
    /// <typeparam name="TSource">Source object</typeparam>
    /// <typeparam name="TTarget">Target object</typeparam>
    public class ObjectMapper<TSource, TTarget> : Mapper<TSource, TTarget>
        where TSource : class
        where TTarget : class
    {

        #region constructor

        /// <summary>
        /// The ObjectMapper Constructor
        /// </summary>
        public ObjectMapper()
        {
            Target = Activator.CreateInstance<TTarget>();
            ParentChildCollection = new List<ParentChild>();
        }

        #endregion

        #region private properties

        /// <summary>
        /// Gets or Sets Target object
        /// </summary>
        private TTarget Target
        {
            get;
            set;
        }

        #endregion

        #region Public methods

        /// <summary>
        /// Maps a source object to a target object
        /// </summary>
        /// <param name="source">The source object</param>
        /// <returns>Mapped target object</returns>
        public TTarget MapToTarget(TSource source)
        {
            RecurseProperties(source, Target);
            return Target;
        }

        /// <summary>
        /// Map to target and consider if shallow copy is required, No collections will be mapped
        /// </summary>
        /// <param name="source">The source object</param>
        /// <param name="considerShallowCopy">If shallow copy</param>
        /// <returns>Mapped target object</returns>
        public TTarget MapToTarget(TSource source, bool considerShallowCopy)
        {
            RecurseProperties(source, Target, considerShallowCopy);
            return Target;
        }

        /// <summary>
        /// Maps collection object
        /// </summary>
        /// <param name="source">Source collection</param>
        /// <returns>Mapped collection object</returns>
        public TTarget MapToTargetCollection(TSource source)
        {
            var enumerator = InvokeMethod(source, "GetEnumerator");
            int noOfRecords = (int)GetPropertyValue(source, "Count");
            int count = 0;
            while (count < noOfRecords)
            {
                var result = InvokeMethod(enumerator, "MoveNext");
                var sourceItem = GetPropertyValue(enumerator, "Current");
                Type targetType = typeof(TTarget).GetGenericArguments()[0];
                Type sourceType = sourceItem.GetType();
                var targetItem = Activator.CreateInstance(targetType);
                RecurseProperties(sourceItem, targetItem);
                Target.GetType().GetMethod("Add").Invoke(Target, new object[] { targetItem });
                count++;
            }
            return Target;
        }

        /// <summary>
        /// Maps collection object consider shallow copy
        /// </summary>
        /// <param name="source">Source collection</param>
        /// <param name="considerShallowCopy">If a shallow copy is reuired, true for required</param>
        /// <returns>Mapped collection object</returns>
        public TTarget MapToTargetCollection(TSource source, bool considerShallowCopy)
        {
            var enumerator = InvokeMethod(source, "GetEnumerator");
            int noOfRecords = (int)GetPropertyValue(source, "Count");
            int count = 0;
            while (count < noOfRecords)
            {
                var result = InvokeMethod(enumerator, "MoveNext");
                var sourceItem = GetPropertyValue(enumerator, "Current");
                Type targetType = typeof(TTarget).GetGenericArguments()[0];
                Type sourceType = sourceItem.GetType();
                var targetItem = Activator.CreateInstance(targetType);
                RecurseProperties(sourceItem, targetItem, considerShallowCopy);
                Target.GetType().GetMethod("Add").Invoke(Target, new object[] { targetItem });
                count++;
            }
            return Target;
        }


        #endregion

        #region private methods

        /// <summary>
        /// The main mapping function that gets called recursively depending on property
        /// </summary>
        /// <param name="source">the source from where properies neeeds to be mapped</param>
        /// <param name="targetObject">the target object</param>
        /// <param name="ignoreCollection">if collections needs to be ignored for shallow copy, true for a shallow copy</param>
        private void RecurseProperties(Object source, object targetObject, bool ignoreCollection = false)
        {
            Type sourceType = source.GetType();
            PropertyInfo[] sourceProperties = sourceType.GetProperties();
            string sourceTypeName = sourceType.Name;
            foreach (var property in sourceProperties)
            {
                var sourceValue = property.GetValue(source, null);
                string propertyName = property.Name;
                if (!string.IsNullOrEmpty(sourceTypeName) && !string.IsNullOrEmpty(propertyName))
                {
                    ParentChildCollection.Add(new ParentChild(sourceTypeName, propertyName));
                }
                if (sourceValue != null)
                {
                    if (sourceValue.GetType().IsGenericType == true)
                    {
                        if (!ignoreCollection)
                        {
                            Type genericItemType = GetTypeFromGenericObject(sourceValue);

                            string genericItemTypeName = genericItemType.Name;
                            if (!string.IsNullOrEmpty(sourceTypeName) && !string.IsNullOrEmpty(genericItemTypeName))
                            {
                                ParentChildCollection.Add(new ParentChild(sourceTypeName, genericItemTypeName));
                            }
                            if (!IsAParent(sourceTypeName, property.Name))
                            {
                                CollectionMapping(property, sourceValue, targetObject);
                            }
                        }
                    }
                    else
                    {
                        if (!IsAParent(sourceTypeName, property.Name))
                        {
                            SetValue(targetObject, property.Name, sourceValue);
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Maps collection object for collection properties
        /// </summary>
        /// <param name="sourceProperty">The collection Property of the source Object</param>
        /// <param name="sourceValue">Value of the property in the source object</param>
        /// <param name="targetObject">The target object</param>
        private void CollectionMapping(PropertyInfo sourceProperty, object sourceValue, object targetObject)
        {
            PropertyInfo matchedProperty = GetMatchedProperty(targetObject, sourceProperty.Name);
            if (matchedProperty != null)
            {
                if (matchedProperty.PropertyType.IsGenericType)
                {
                    var proxyHashSetEnumerator = InvokeMethod(sourceValue, "GetEnumerator");
                    int noOfRecords = (int)GetPropertyValue(sourceValue, "Count");
                    Type pocoItemType = matchedProperty.PropertyType.GetGenericArguments()[0];
                    Type pocoItemListType = GetGenericListType(pocoItemType);
                    object pocoItemListTypeInstance = Activator.CreateInstance(pocoItemListType);
                    int count = 0;
                    while (count < noOfRecords)
                    {
                        var result = InvokeMethod(proxyHashSetEnumerator, "MoveNext");
                        var item = GetPropertyValue(proxyHashSetEnumerator, "Current");
                        var pocoItemTypeInstance = Activator.CreateInstance(pocoItemType);
                        RecurseProperties(item, pocoItemTypeInstance);
                        pocoItemListTypeInstance.GetType().GetMethod("Add").Invoke(pocoItemListTypeInstance, new object[] { pocoItemTypeInstance });
                        count++;
                    }
                    matchedProperty.SetValue(targetObject, pocoItemListTypeInstance);
                }
            }

        }

        #endregion
    }
}
